<?php

if (!class_exists("FineLog")) {

require_once(dirname(__FILE__) . "/except.IOException.php");

/**
 * Objet de gestion des messages de log.
 *
 * Cet objet s'utilise de manière statique, pour écrire dans un fichier de log centralisé.
 * Il s'utilise de cette manière :
 * <code>
 * // utilisation basique, niveau de criticité INFO par défaut 
 * FineLog::log("Message de log");
 * // utilisation avancée, en spécifiant la crititité
 * FineLog::log(FineLog::WARN, "Message de warning");
 * // utilisation complète, en spécifiant la criticité, le nom de fichier, le numéro
 * // de ligne, et le nom d'objet et de méthode appelants.
 * FineLog::fullLog(FineLog::ERROR, "Message d'erreur", __FILE__, __LINE__, __METHOD__);
 * // identique à ci-dessus, mais en spécifiant le nom de la fonction appelante (procédural)
 * FineLog::fullLog(FineLog::ERROR, "Message d'erreur", __FILE__, __LINE__, __FUNCTION__);
 * // identique à ci-dessus, sans spécifier de méthode ni de fonction (appel depuis l'exécution globale)
 * FineLog::fullLog(FineLog::ERROR, "Message d'erreur", __FILE__, __LINE__, "");
 * </code>
 *
 * Il est aussi possible de définir le seuil de criticité. Tous les message dont le niveau
 * sera inférieur à ce seuil ne seront pas enregistrés.
 * Il existe 6 niveaux de criticité :
 * - DEBUG : message de débuggage (criticité la plus faible)
 * - INFO : message d'information (niveau par défaut)
 * - NOTE : notification ; message normal mais significatif (seuil par défaut)
 * - WARN : message d'alerte ; l'application ne fonctionne pas normalement mais elle peut continuer à fonctionner.
 * - ERROR : message d'erreur ; l'application ne fonctionne pas normalement et elle doit s'arrêter.
 * - CRIT : message d'erreur critique ; l'application risque d'endommager son environnement (filesystem ou base de données).
 * <code>
 * // définition du seuil
 * FineLog::setThreshold(FineLog::INFO);
 * // ce message ne sera pas écrit
 * FineLog::log(FineLog::DEBUG, "Message de débug");
 * // ce message sera écrit
 * FineLog::log(FineLog::NOTE, "Notification);
 * </code>
 *
 * Le chemin vers le fichier de log peut être défini de 2 manières :
 * - dans la variable d'environnement FINE_LOG_PATH
 * - en appelant la méthode FineLog::setLogFile($path)
 * La seconde méthode est recommandée, car elle permet d'utiliser une autre fonctionnalité, les classes de log.
 *
 * Il est possible de désigner un ensemble de classes de log, qui sont des labels permettant de regrouper les logs.
 * Les logs de toutes les classes se retrouveront dans le même fichier, mais il est possible de définir un seuil
 * différent pour chaque classe.
 * <code>
 * // initialisation des seuils de log en fonction des classes
 * $thresholds = array(
 *         "default" => FineLog::ERROR,
 *         "testing" => FineLog::DEBUG
 * );
 * FineLog::setThreshold($thresholds);
 * // ce message n'atteint pas le seuil nécessaire, il ne sera pas écrit
 * FineLog::log(FineLog::WARN, "Message sans classe");
 * // ce message sera écrit
 * FineLog::log("default", FineLog::ERROR, "Message utilisant la classe par défaut");
 * // ce message sera écrit
 * FineLog::log("testing", FineLog::CRIT, "Message applicatif");
 *
 * // les logs étendus fonctionnent aussi avec les classes de log
 * FineLog::fullLog("testing", FineLog::NOTE, "Notification", __FILE__, __LINE__, __METHOD__);
 * </code>
 * A noter :
 * - Les messages sans classe désignée sont liés à la classe "default".
 * - Si on écrit un message de log en spécifiant une classe qui n'a pas été définie, le message ne sera écrit nulle part.
 *
 * @author	Amaury Bouchard <amaury.bouchard@finemedia.fr>
 * @copyright	Copyright (c) 2007, FineMedia
 * @package	FineFS
 * @subpackage	utils
 * @version	$Id$
 */
class FineLog {
	/** Constante - message de débuggage (priorité la plus faible). */
	const DEBUG = 10;
	/** Constante - message d'information (niveau par défaut). */
	const INFO = 20;
	/** Constante - notification ; message normal mais significatif (seuil par défaut). */
	const NOTE = 30;
	/** Constante - message d'alerte ; l'application ne fonctionne pas normalement mais elle peut continuer à fonctionner. */
	const WARN = 40;
	/** Constante - message d'erreur ; l'application ne fonctionne pas normalement et elle doit s'arrêter. */
	const ERROR = 50;
	/** Constante - message d'erreur critique ; l'application risque d'endommager son environnement (filesystem ou base de données). */
	const CRIT = 60;
	/** Nom de la classe de log par défaut. */
	const DEFAULT_CLASS = "default";
	/** Chemin vers le fichier dans lequel écrire les messages de log. */
	static private $_logPath = null;
	/** Seuil actuel de criticité des messages affichés. */
	static private $_threshold = self::NOTE;
	/** Tableau contenant les labels correspondant aux niveaux de log. */
	static private $_labels = array(
		10 => "DEBUG",
		20 => "INFO ",
		30 => "NOTE ",
		40 => "WARN ",
		50 => "ERROR",
		60 => "CRIT "
	);

	/* ******************** METHODES PUBLIQUES ****************** */
	/**
	 * Définit le chemin vers le fichier de log à utiliser.
	 * @param	string	path	Chemin vers le fichier de log.
	 * @throws	ApplicationException
	 */
	static public function setLogFile($path) {
		self::$_logPath = $path;
	}
	/** Définit l'utilisation de la sortie standard pour les logs. */
	static public function setStdout() {
		self::$_logPath = "php://stdout";
	}
	/** Définit l'utilisation de la sortie d'erreur pour les logs. */
	static public function setStderr() {
		self::$_logPath = "php://stderr";
	}
	/**
	 * Définit le seuil de criticité minimum en-dessous duquel les messages ne sont pas écrits.
	 * @param	int|array	$threshold	Valeur de seuil (FineLog::DEBUG, FineLog::INFO, ...) ou
	 *						liste de seuils pour chaque classe de log.
	 */
	static public function setThreshold($threshold) {
		if (is_array($threshold))
			self::$_threshold = $threshold;
		else {
			self::$_threshold = array();
			self::$_threshold[self::DEFAULT_CLASS] = $threshold;
		}
	}
	/**
	 * Ecrit un message de log, soit en spécifiant le niveau de priorité, soit avec le niveau INFO.
	 * A n'utiliser que pour ajouter rapidement un message de log temporaire.
	 * Utilisez la méthode fullLog() pour les messages définitifs.
	 * @param	mixed	$classOrMessageOrPriority	essage de log ou niveau de priorité du log ou classe de log.
	 * @param	mixed	$messageOrPriority		(optionnel) Message de log ou niveau de priorité du log.
	 * @param	string	$message				(optionnel) Message de log à écrire.
	 */
	static public function log($classOrMessageOrPriority, $messageOrPriority=null, $message=null) {
		if (!is_null($message) && !is_null($messageOrPriority))
			self::_writeLog($classOrMessageOrPriority, $messageOrPriority, $message);
		else if (!is_null($messageOrPriority))
			self::_writeLog(self::DEFAULT_CLASS,  $classOrMessageOrPriority, $messageOrPriority);
		else
			self::_writeLog(self::DEFAULT_CLASS, self::INFO, $classOrMessageOrPriority);
	}
	/**
	 * Ecrit un message de log détaillé.
	 * Le premier paramètre est optionnel.
	 * @param	string	$classOrPriority	Classe de log ou niveau de priorité du message.
	 * @param	int	$priorityOrMessage	Niveau de priorité du message, ou message de log à écrire.
	 * @param	string	$messageOrFile		Message de log à écrire ou nom du fichier où la méthode a été appelée.
	 * @param	string	$fileOrLine		Nom du fichier ou numéro de la ligne où la méthode a été appelée.
	 * @param	int	$lineOrCaller		(optionnel) Numéro de la ligne où la méthode a été appelée ou nom de la
	 *						fonction ou de la méthode appelante.
	 * @param	string	$caller			(optionnel) Nom de la fonction ou de la méthode appelante (optionnel).
	 */
	static public function fullLog($classOrPriority, $priorityOrMessage, $messageOrFile, $fileOrLine, $lineOrCaller, $caller=null) {
		if (is_null($caller)) {
			// 5 paramètres : pas de classe de log
			$caller = $lineOrCaller;
			$line = $fileOrLine;
			$file = $messageOrFile;
			$message = $priorityOrMessage;
			$priority = $classOrPriority;
			$class = self::DEFAULT_CLASS;
		} else {
			// 6 paramètres : classe de log
			$line = $lineOrCaller;
			$file = $fileOrLine;
			$message = $messageOrFile;
			$priority = $priorityOrMessage;
			$class = $classOrPriority;
		}
		$txt = "[" . basename($file) . ":$line]";
		if (!empty($caller))
			$txt .= " $caller()";
		self::_writeLog($class, $priority, "$txt: $message");
	}

	/* ********************** METHODES PRIVEES *************** */
	/**
	 * Ecrit un message dans le fichier de log si sa priorité atteint le seuil défini.
	 * @param	string	$class		Classe de log du message.
	 * @param	int	$priority	Niveau de priorité du message.
	 * @param	string	$message	Message de log à écrire.
	 * @throws	IOException
	 */
	static private function _writeLog($class, $priority, $message) {
		// le message n'est pas écrit si sa priorité est inférieure au seuil défini
		if (!isset(self::$_threshold[$class]) || $priority < self::$_threshold[$class])
			return;
		// ouvre le fichier si nécessaire
		if (!empty(self::$_logPath))
			$path = self::$_logPath;
		else if (!empty($_SERVER["FINE_LOG_PATH"]))
			$path = $_SERVER["FINE_LOG_PATH"];
		else
			throw new ApplicationException("No log file set.", ApplicationException::API);
		$text = date("c") . " " . self::$_labels[$priority] . " ";
		if (!empty($class) && $class != self::DEFAULT_CLASS)
			$text .= "-$class- ";
		$text .= $message . "\n";
		if (file_put_contents($path, $text, (substr($path, 0, 6) != "php://" ? FILE_APPEND : null)) === false)
			throw new IOException("Unable to write on log file '$path'.", IOException::UNWRITABLE);
	}
}

} // class_exists

?>
